/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-会话管理-reactor-缺省实现
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.reactor.session.autoconfigure;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.taipingframework.boot.cache.core.ReactiveCacheHandler;
import com.taipingframework.boot.cache.redis.StandardCacheKeyBuilder;
import com.taipingframework.boot.reactor.cache.autoconfigure.SpringCacheReactorAutoConfiguration;
import com.taipingframework.boot.reactor.session.DefaultSessionHandler;
import com.taipingframework.boot.reactor.session.filters.WebSessionFilter;
import com.taipingframework.boot.session.ReactorSessionManager;
import com.taipingframework.boot.session.core.ReactiveSessionHandler;
import com.taipingframework.boot.session.properties.HttpSessionProperty;
import com.taipingframework.utility.thread.properties.ThreadPoolProperty;
import com.taipingframework.utility.thread.threadfactory.SmartThreadFactory;
import com.taipingframework.utility.thread.threadpool.ThreadPoolHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.web.server.session.HeaderWebSessionIdResolver;
import org.springframework.web.server.session.WebSessionIdResolver;

// import org.springframework.session.FlushMode;
// import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
// import org.springframework.session.data.redis.config.ConfigureRedisAction;
// import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
// import org.springframework.session.web.http.HttpSessionIdResolver;

/**
 * 注解@EnableRedisHttpSession的参数maxInactiveIntervalInSeconds作用：
 * （1）自动化配置 Spring Session 使用 Redis 作为数据源
 * （2）设置SpringSession的过期时间(单位：秒)
 * （3）设置session在redis存储的命名空间
 * 。。。 。。。[想了解更多，就去看注解内部定义的参数]
 */
@Slf4j
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)

@ConditionalOnProperty(
        prefix = "application.session",
        name = {"enable"},
        havingValue = "true",
        matchIfMissing = true
)
@AutoConfigureAfter(SpringCacheReactorAutoConfiguration.class)
@AutoConfigureBefore(SessionAutoConfiguration.class)

// 请注意，经过广大网友和自己亲自验证：
// spring.session.redis.flush-mode (对应参数 flushMode) 与 spring.session.timeout (对应参数 maxInactiveIntervalInSeconds)
// 以及 spring.session.redis.namespace (对应参数redisNamespace，此处需要通过编码读取application.yaml的配置并设置)
// 只在配置文件中设置是不会生效的，必须在此处通过 注解@EnableRedisHttpSession的这三个参数 进行配置才会真正起作用。
// 另外，当前配置文件中被注掉的部分编码实际上也不会被执行。
// 利用spring-session管理会话，在当前版本还需要多多珍重啊。。。

// ● SERVLET模式
// @EnableRedisHttpSession(flushMode = FlushMode.IMMEDIATE, maxInactiveIntervalInSeconds = 2400, redisNamespace = "${spring.session.redis.namespace}")

// ● REACTIVE模式
// 针对Reactive Web，即Spring Webflux，其实就是针对spring-cloud-gateway的配置:
// 具体参考【https://baijiahao.baidu.com/s?id=1612733571967993309&wfr=spider&for=pc】和【https://blog.csdn.net/hsf1982/article/details/126988807】
@EnableRedisWebSession(maxInactiveIntervalInSeconds = 2700, redisNamespace = "${spring.session.redis.namespace}")

public class SpringSessionReactorAutoConfiguration {

    /**
     * 自定义 Spring-Session Starter 配置参数
     */
    @Bean
    @ConfigurationProperties(prefix = "application.session")
    @ConditionalOnProperty(prefix = "application.session", name = {"enable"}, havingValue = "true", matchIfMissing = true)
    public HttpSessionProperty httpSessionProperty() {
        return new HttpSessionProperty();
    }

    /**
     * spring-webflux环境下的filter过滤器
     */
    @Bean("webSessionFilter")
    @ConditionalOnProperty(prefix = "application.flux.filter", name = {"enable-web-session-filter"}, havingValue = "true", matchIfMissing = true)
    public WebSessionFilter webSessionFilter() {
        return new WebSessionFilter();
    }

    /**
     * WebSession会话控制
     */
    @Bean("reactorSessionManager")
    public ReactorSessionManager reactorSessionManager(
            @Autowired @Qualifier("reactiveCacheHandler") ReactiveCacheHandler reactiveCacheHandler) {
        return new ReactorSessionManager(reactiveCacheHandler);
    }

    /**
     * 默认的标准会话管理处理器
     */
    @Bean("reactiveSessionHandler")
    public ReactiveSessionHandler reactiveSessionHandler(
            @Autowired @Qualifier("standardCacheKeyBuilder") StandardCacheKeyBuilder standardCacheKeyBuilder,
            @Autowired @Qualifier("reactiveCacheHandler") ReactiveCacheHandler reactiveCacheHandler,
            @Autowired @Qualifier("objectMapper") ObjectMapper objectMapper) {
        // DefaultSessionHandler在初始化时依赖StandardCacheKeyBuilder
        return new DefaultSessionHandler(reactiveCacheHandler, objectMapper);
    }

    /**
     * 采用 JSON 序列化方式。
     * <p>
     * 创建 {@link org.springframework.session.data.redis.RedisOperationsSessionRepository} 或
     * {@link org.springframework.session.data.redis.RedisIndexedSessionRepository} 时
     * 使用的 RedisSerializer Bean 。
     * <p>
     * 具体请参考源码{@link org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration}中
     * 的方法 setDefaultRedisSerializer(RedisSerializer<Object>)。
     */
    @Bean(name = "springSessionDefaultRedisSerializer")
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return RedisSerializer.json();
    }

    /*
     * ● SERVLET模式
     * <p>
     * SessionId 解析器，自定义SessionId存放位置，基于 http请求头header 参数解析 sessionId
     * 要求每次HTTP请求的请求头必须携带参数`TOKEN`，也就是此处参数 headerName 指定的 参数名。
     * <p>
     * 具体请参考源码{@link org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration}中
     * 的方法 setHttpSessionIdResolver(HttpSessionIdResolver)。
     *
    @Bean("sessionIdResolver")
    public HttpSessionIdResolver sessionIdResolver(@Value("${application.session.header-name:TOKEN}") String headerName) {
        return new HeaderHttpSessionIdResolver(headerName);
    }*/

    /**
     * ● REACTIVE模式
     * <p>
     * SessionId 解析器，自定义SessionId存放位置，基于 http请求头header 参数解析 sessionId
     * 要求每次HTTP请求的请求头必须携带参数`TOKEN`，也就是此处参数 headerName 指定的 参数名。
     */
    @Bean("webSessionIdResolver")
    public WebSessionIdResolver webSessionIdResolver(@Value("${application.session.header-name:TOKEN}") String headerName) {
        HeaderWebSessionIdResolver headerWebSessionIdResolver = new HeaderWebSessionIdResolver();
        headerWebSessionIdResolver.setHeaderName(headerName);
        return headerWebSessionIdResolver;
    }

    /**
     * 用于spring session，防止每次创建一个线程，该线程池不是必须的，如果不自定义默认将使用SimpleAsyncTaskExecutor线程池。
     * <p>
     * 具体请参考源码{@link org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration}中
     * 的方法 setRedisTaskExecutor(Executor)。
     * <p>
     * 原因：解决Spring session(redis存储方式)监听导致创建大量redisMessageListenerContainer-X线程问题。
     * SimpleAsyncTaskExecutor线程池并非真正的池化，它每次执行任务都会创建一个新的线程，高并发场景下容易引发内存溢出异常，
     * 另外，在监听redis keyEvent事件时频繁创建线程，也会导致性能消耗高，CPU占用率极高这类问题。
     */
    @Bean(value = "springSessionRedisTaskExecutor", destroyMethod = "destroy")
    @ConditionalOnBean(ThreadPoolProperty.class)
    public ThreadPoolTaskExecutor springSessionRedisTaskExecutor(ThreadPoolProperty poolProperty) {
        SmartThreadFactory threadFactory = new SmartThreadFactory(poolProperty.getThreadPrefix());
        return ThreadPoolHelper.createAsyncExecutor(
                poolProperty.getCorePoolSize(),
                poolProperty.getMaxPoolSize(),
                poolProperty.getKeepAliveTime(),
                poolProperty.getQueueCapacity(),
                threadFactory);
    }

    /**
     * 用于spring session，防止每次创建一个线程，该线程池不是必须的，如果不自定义默认将使用SimpleAsyncTaskExecutor线程池。
     * <p>
     * 具体请参考源码{@link org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration}中
     * 的方法 setRedisSubscriptionExecutor(Executor)。
     * <p>
     * 原因：解决Spring session(redis存储方式)监听导致创建大量redisMessageListenerContainer-X线程问题。
     * SimpleAsyncTaskExecutor线程池并非真正的池化，它每次执行任务都会创建一个新的线程，高并发场景下容易引发内存溢出异常，
     * 另外，在监听redis keyEvent事件时频繁创建线程，也会导致性能消耗高，CPU占用率极高这类问题。
     */
    @Bean(value = "springSessionRedisSubscriptionExecutor", destroyMethod = "destroy")
    @ConditionalOnBean(ThreadPoolProperty.class)
    public ThreadPoolTaskExecutor springSessionRedisSubscriptionExecutor(ThreadPoolProperty poolProperty) {
        SmartThreadFactory threadFactory = new SmartThreadFactory(poolProperty.getThreadPrefix());
        return ThreadPoolHelper.createAsyncExecutor(
                poolProperty.getCorePoolSize(),
                poolProperty.getMaxPoolSize(),
                poolProperty.getKeepAliveTime(),
                poolProperty.getQueueCapacity(),
                threadFactory);
    }

    /**
     * 监听session创建
     */
    @EventListener
    public void onCreated(SessionCreatedEvent event) {
        String sessionId = event.getSessionId();

        // 经过验证，该监听事件实际上是被触发了的。
        // 而 SessionDeletedEvent 和 SessionExpiredEvent 并未被触发！！！
        log.debug("Spring-Session事件监听器 - 创建Session - {}", sessionId);
    }

    /**
     * 监听session删除
     */
    @EventListener
    public void onDeleted(SessionDeletedEvent event) {
        String sessionId = event.getSessionId();

        // 经过验证，该监听事件最终并未被触发
        log.debug("Spring-Session事件监听器 - 删除Session - {}", sessionId);
    }

    /**
     * 监听session过期
     */
    @EventListener
    public void onExpired(SessionExpiredEvent event) {
        try {
            String sessionId = event.getSessionId();

            // 经过验证，该监听事件最终并未被触发
            log.debug("Spring-Session事件监听器 - 失效Session - {}", sessionId);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /*
     * spring session默认会自动开启session事件通知配置
     * 如果不需要自动开启该配置的话可以将 bean `configureRedisAction` 配置加入到容器中
     *
    @Bean
    public ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }*/

}
